//++
//
// Copyright (c) Microsoft Corporation.  All rights reserved.
//
// Module Name:
//
//  OutlookReminderInspector.cs
//    
// Abstract:
//
//  This class tracks the state of an Outlook inspector window for the add-in
//  
//--
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.Office.Interop.Outlook;
using Microsoft.OfficeCommunicationsServer.Applications.Common;

using MeetingException = Microsoft.Office.Interop.Outlook.Exception;

namespace Microsoft.EnhancedReminders.OutlookAddIn
{
    /// <summary>
    /// This class tracks the state of an Outlook inspector window for the 
    /// add-in and ensures that Ribbon and item state is handled correctly.
    /// </summary>
    // Note: this class is public so unit tests can instantiate it.
    public sealed class OutlookReminderInspector : IExchangeSession, IDisposable
    {
        private static readonly string ReminderPropertyName = String.Format(CultureInfo.InvariantCulture, 
            "http://schemas.microsoft.com/mapi/string/{{{0}}}/{1}", Constants.ReminderPropertyId, Constants.ReminderPropertyName);
        private static readonly string ReminderRuleIdPropertyName = String.Format(CultureInfo.InvariantCulture, 
            "http://schemas.microsoft.com/mapi/string/{{{0}}}/{1}", Constants.ReminderRuleIdPropertyId, Constants.ReminderRuleIdPropertyName);
        private static readonly string JoinNowRuleIdPropertyName = String.Format(CultureInfo.InvariantCulture,
            "http://schemas.microsoft.com/mapi/string/{{{0}}}/{1}", Constants.JoinNowRuleIdPropertyId, Constants.JoinNowRuleIdPropertyName);
        private static readonly string ReminderOnExceptionDeletedPropertyName = String.Format(CultureInfo.InvariantCulture,
            "http://schemas.microsoft.com/mapi/string/{{{0}}}/{1}", Constants.ReminderOnExceptionDeletedPropertyId, Constants.ReminderOnExceptionDeletedPropertyName);

        #region Instance Variables

        private Inspector _window;                         // wrapped window object
        private AppointmentItem _appointment;              // wrapped ContactItem
        private ReminderItem _reminderItem;
        private bool _reminderChanged;

        private BackgroundWorker _backgroundWorker;
        private AutoResetEvent _backgroundWorkerCompletedEvent;

        private EnhancedRemindersRibbon _ribbon;

        #endregion

        #region Properties

        /// <summary>
        /// EnhancedReminderRibbon object
        /// </summary>
        internal EnhancedRemindersRibbon Ribbon
        {
            set
            {
                _ribbon = value;
            }
        }

        /// <summary>
        /// Returns whether or not it is appropriate to enable this appointment for Join Now
        /// </summary>
        internal bool IsJoinNow
        {
            get
            {
                if (_appointment.MessageClass == Constants.LiveMeetingMessageClass)
                {
                    return true;
                }

                if (_appointment.MessageClass == Constants.RecurringMeetingExceptionMessageClass)
                {
                    AppointmentItem recurrenceMaster =
                        Helpers.AssertCast<AppointmentItem>(_appointment.GetRecurrencePattern().Parent);

                    return recurrenceMaster.MessageClass == Constants.LiveMeetingMessageClass;
                }

                return false;
            }
        }

        /// <summary>
        /// Returns the lead time asssociated with this reminder.
        /// </summary>
        internal int LeadTime
        {
            get
            {

                if (_reminderItem.NotificationChannelList is NotificationChannelListAbsoluteTime)
                {
                    // Convert the absolute time to a lead time based on the current start time of the appointment
                    TimeSpan leadTimeSpan = _reminderItem.GetDueTime(_appointment.StartUTC, false) - _appointment.StartUTC;
                    
                    return (int)leadTimeSpan.TotalMinutes;
                }
                else
                {
                    return _reminderItem.NotificationChannelList.AssertCast<NotificationChannelListLeadTime>().LeadTime;
                }
            }
            set
            {
                if (_reminderItem.NotificationChannelList is NotificationChannelListAbsoluteTime)
                {
                    // Convert the absolute time data structure to the equivalent relative time data structure
                    _reminderItem.NotificationChannelList = new NotificationChannelListLeadTime(_reminderItem.NotificationChannelList.Channels);
                }

                _reminderItem.NotificationChannelList.AssertCast<NotificationChannelListLeadTime>().LeadTime = value;
            }
        }

        /// <summary>
        /// Returns the lead time associated with the join now portion of this reminder.
        /// </summary>
        internal int JoinNowLeadTime
        {
            get
            {
                if (_reminderItem.JoinNowChannelList != null)
                {
                    return _reminderItem.JoinNowChannelList.LeadTime;
                }
                else
                {
                    return NotificationChannelListLeadTime.None;
                }
            }
            set
            {
                _reminderItem.JoinNowChannelList.LeadTime = value;
            }
        }

        /// <summary>
        /// Returns the channels associated with this reminder.
        /// </summary>
        internal List<NotificationChannel> Channels
        {
            get
            {
                return _reminderItem.NotificationChannelList.Channels;
            }
        }

        /// <summary>
        /// Returns the channels associated with the join now portion of this reminder.
        /// </summary>
        internal List<NotificationChannel> JoinNowChannels
        {
            get
            {
                return _reminderItem.JoinNowChannelList.Channels;
            }
        }

        /// <summary>
        /// Indicates if the details of the enhnaced reminder have been changed. This is used to determine if a rules-based 
        /// reminder has been manually edited and is therefore no longer a rules-based reminder.
        /// </summary>
        internal bool ReminderChanged
        {
            set
            {
                _reminderChanged = value;
            }
        }

        /// <summary>
        /// True if the enhanced reminder is for an exception to a recurring meeting or appointment.
        /// </summary>
        private bool IsRecurringMeetingException
        {
            get
            {
                Debug.Assert(_appointment != null, "_appointment");
                Debug.Assert(_appointment.MessageClass != Constants.RecurringMeetingExceptionMessageClass ||
                             _appointment.IsRecurring,
                             "Only recurring meetings or appointments can have exceptions.");

                return _appointment.MessageClass == Constants.RecurringMeetingExceptionMessageClass;
            }
        }

        /// <summary>
        /// Syncs the channel lists in the reminder item to the master channel list.
        /// </summary>
        internal void SyncChannelsToMasterList()
        {
            if (_reminderItem != null)
            {
                List<NotificationChannel> channels = ReminderOutlookUtils.LoadChannels(new OutlookApplication(Globals.ThisAddIn.Application), /*restoreDefaultsIfNecessary*/false);

                _reminderItem.NotificationChannelList.SyncToMasterChannelsList(channels, false, true);
                if (_reminderItem.JoinNowChannelList != null)
                {
                    _reminderItem.JoinNowChannelList.SyncToMasterChannelsList(channels, true, true);
                }
            }
        }

        /// <summary>
        /// The actual Outlook inspector window wrapped by this instance
        /// </summary>
        internal Inspector Window
        {
            get 
            { 
                return _window; 
            }
        }

        #endregion

        #region Events

        /// <summary>
        /// Event raised when the Inspector window is closed.
        /// </summary>
        public event EventHandler Closed;

        #endregion

        #region Constructor

        /// <summary>
        /// Create a new instance of the tracking class for an inspector 
        /// </summary>
        /// <param name="inspector">The new inspector window to monitor</param>
        public OutlookReminderInspector(Inspector inspector)
        {
            Debug.Assert(null != inspector, "inspector is not null");

            _window = inspector;
            _appointment = Helpers.AssertCast<AppointmentItem>(inspector.CurrentItem);

            // Check if the appointment is an exception to a recurring meeting.  For exceptions
            // various properties need to be copied over from the "master" recurring appointment.
            CopyInfoFromRecurringMeetingMaster();
           
            // Hookup events
            _appointment.Write += AppointmentWrite;

            InspectorEvents_Event inspectorEvent = Helpers.AssertCast<InspectorEvents_Event>(inspector);
            inspectorEvent.Close += OutlookWindow_Closed;

            // Initialize background worker
            _backgroundWorker = new BackgroundWorker();
            _backgroundWorker.DoWork += ExecuteBackgroundOperations;
            _backgroundWorker.RunWorkerCompleted += BackgroundOperationsCompleted;

            _backgroundWorkerCompletedEvent = new AutoResetEvent(false);

            // Start background worker operation. 
            // Pass in false to indicate deserializing ReminderItem from attachment
            _backgroundWorker.RunWorkerAsync(false /*isSerializing*/);
        }

        #endregion

        #region Event Handlers

        /// <summary>
        /// If the appointment item represents an exception to a recurring meeting, copy the reminder
        /// from the recurring meeting master if this is the first time the exception is encountered.
        /// </summary>
        /// <remarks>
        /// Do not override the MessageClass property for the exception.  Ideally we would copy the 
        /// master value of this property (to denote a Live Meeting for example).  It seems that Outlook
        /// relies on this property to detect that a meeting is an exception and the behaviour of the
        /// calendar item changes (can no longer open the meeting series or delete the meeting) if the
        /// property is overridden (IsRecurring becomes false in this case also).
        /// </remarks>
        private void CopyInfoFromRecurringMeetingMaster()
        {
            if (!IsRecurringMeetingException)
            {
                return;
            }

            AppointmentItem recurrenceMaster =
                Helpers.AssertCast<AppointmentItem>(_appointment.GetRecurrencePattern().Parent);

            foreach (MeetingException meetingException in recurrenceMaster.GetRecurrencePattern().Exceptions)
            {
                Debug.Assert(meetingException.AppointmentItem != null, "meetingException.AppointmentItem");

                if (meetingException.AppointmentItem.Equals(_appointment))
                {
                    return; // Only copy data from the master when the exception is first created (before it's added to the exception list).
                }
            }

            // Copy the reminder from the master.  Only do this the first time the exception is created to
            // avoid overriding the exception's value if the user sets it to no reminder (reminder property is null)
            // but the master has a reminder.
            string reminder = GetProperty(recurrenceMaster, ReminderPropertyName);
            if (reminder != null)
            {
                _appointment.PropertyAccessor.SetProperty(ReminderPropertyName, reminder);
            }
        }

        /// <summary>
        /// Event to indicate that changes made to the appointment is saved
        /// </summary>
        private void AppointmentWrite(ref bool cancel)
        {
            Debug.Assert(_backgroundWorker != null, "_backgroundWorker != null");
            Debug.Assert(_backgroundWorkerCompletedEvent != null, "_backgroundWorkerCompletedEvent != null");

            // WaitOne() resets the AutoResetEvent object but RunWorkerAsync will start 
            // the backgroundworker which upon completion, will set the AutoResetEvent object
            // for anything waiting thread (either in this method or in OutlookWindow_Closed
            // where the backgroundworker is to be disposed).
            
            // Outlook throws COMException occasionally when doing this via the BackgroundWorker thread
            // The workaround is to execute this on the main thread, but we still have to wait for the
            // BackgroundWorker to complete it's execution
            _backgroundWorkerCompletedEvent.WaitOne();
            SaveChanges();
            _backgroundWorkerCompletedEvent.Set();
        }

        /// <summary>
        /// Event Handler for the inspector close event.
        /// </summary>
        private void OutlookWindow_Closed()
        {
            Debug.Assert(null != _window, "Inspector is null");

            // Unhook events from the window
            InspectorEvents_Event inspectorEvent = Helpers.AssertCast<InspectorEvents_Event>(_window);
            inspectorEvent.Close -= OutlookWindow_Closed;

            // Raise the OutlookInspector close event
            if (Closed != null)
            {
                Closed(this, EventArgs.Empty);
            }

            _window = null;

            Debug.Assert(_backgroundWorker != null, "_backgroundWorker != null");
            // Wait until backgroundworker has finished execution
            // The AutoResetEvent will be set by the background worker thread which is 
            // started at the constructor, and whenever we're saving changes to the
            // ReminderItem
            _backgroundWorkerCompletedEvent.WaitOne();
            _backgroundWorker.Dispose();
            _backgroundWorker = null;

            Debug.Assert(_appointment != null, "_appointment != null");
            _appointment.Write -= AppointmentWrite;

            // Required to ensure Outlook updates the appointment if the reminder item has changed
            // in the meantime.  The scheduler might have just applied a calendar rule and updated 
            // this appointment in the background.  Not releasing this object prevents us from
            // viewing the updated reminder.
            OutlookUtils.ForceRelease(_appointment);
            _appointment = null;
            GC.Collect();
            GC.WaitForPendingFinalizers();         
        }

        #endregion

        #region Helpers

        /// <summary>
        /// Save any changes to the enhanced reminder or JoinNow.
        /// </summary>
        internal void SaveChanges()
        {
            Debug.Assert(_appointment != null, "Appointment is not null");
            Debug.Assert(_reminderItem != null, "_reminderItem != null");

            if (!_reminderChanged)
            {
                return;
            }

            if (_reminderItem.IsEnabled)
            {
                // The enhanced reminder has been manually edited so can no longer be considered a rules-based reminder. 
                // The rule ID properties need to be deleted just in case this was a rules-based reminder. This ensures that
                // the reminder is not updated by the server when the rules are changed or when the calendar item is moved.
                DeleteProperty(ReminderRuleIdPropertyName);
                DeleteProperty(JoinNowRuleIdPropertyName);
                DeleteProperty(ReminderOnExceptionDeletedPropertyName);

                // Serialize and encode the enhanced reminder ready for storing
                string encodedReminderItem = EnhancedRemindersHelpers.SerializeReminderItem(Context.Global, _reminderItem);
                Debug.Assert(!String.IsNullOrEmpty(encodedReminderItem), "!String.IsNullOrEmpty(encodedReminderItem)");

                // Store the enhanced reminder in a custom property
                _appointment.PropertyAccessor.SetProperty(ReminderPropertyName, encodedReminderItem);
            }
            else 
            {
                // The reminder has been disabled so delete all the reminder properties
                DeleteProperty(ReminderPropertyName);
                DeleteProperty(ReminderRuleIdPropertyName);
                DeleteProperty(JoinNowRuleIdPropertyName);
                DeleteProperty(ReminderOnExceptionDeletedPropertyName);

                if (IsRecurringMeetingException)
                {
                    // In the case of an exception to a recurring meeting we want to flag that the reminder
                    // was explicitly deleted to prevent the scheduler from copying the reminder from the
                    // recurring master (e.g. if the master has a reminder but we don't want one on the
                    // exception).  If a meeting exception is created outside of Outlook (e.g. OWA) then
                    // the reminder won't have been copied over, this flag won't have been set and we
                    // will want the scheduler to copy the reminder from the master.
                    _appointment.PropertyAccessor.SetProperty(ReminderOnExceptionDeletedPropertyName, bool.TrueString);
                }
            }
        }

        /// <summary>
        /// Helper method to gets the specified property from the appointment.
        /// </summary>
        /// <param name="schemaName">Name of the property to be obtained.</param>
        /// <param name="appointment">The appointment on which to look up the property.</param>
        private static string GetProperty(AppointmentItem appointment, string schemaName)
        {
            Debug.Assert(appointment != null, "_appointment != null");
            Debug.Assert(!String.IsNullOrEmpty(schemaName), "schemaName");

            string property = null;

            try
            {
                property = appointment.PropertyAccessor.GetProperty(schemaName) as string;
            }
            catch (COMException)
            {
                // Ignore this exception because it just means that the property does not exist
            }

            return property;
        }

        /// <summary>
        /// Deletes the specified property.
        /// </summary>
        /// <param name="schemaName">Name of the property to be deleted</param>
        private void DeleteProperty(string schemaName)
        {
            Debug.Assert(_appointment != null, "_appointment != null");

            try
            {
                _appointment.PropertyAccessor.DeleteProperty(schemaName);
            }
            catch (COMException)
            {
                // Ignore this exception because it just means that the property does not exist
            }
        }

        #endregion

        #region Worker thread

        /// <summary>
        /// Load ReminderItem from property if it exists. If loading fails or it doesn't exist, create 
        /// a new ReminderItem.
        /// </summary>
        private void LoadReminder()
        {
            Debug.Assert(_appointment != null, "_appointment != null");
            Debug.Assert(_reminderItem == null, "_reminderItem == null");

            string reminderPropertyValue = null;

            try
            {
                // Retrieve the enhnaced reminder from the appropriate custom property
                reminderPropertyValue = _appointment.PropertyAccessor.GetProperty(ReminderPropertyName) as string;
            }
            catch (COMException)
            {
                // Ignore this exception because it just means that a reminder has not been attached yet
            }

            if (reminderPropertyValue != null)
            {
                // De-serialize and decode the reminder from the value of the property
                _reminderItem = EnhancedRemindersHelpers.DeserializeReminderItem(Context.Global, reminderPropertyValue);
            }

            if (_reminderItem == null)
            {
                // If no attachment is found, create a new one.
                List<NotificationChannel> channels =
                    Globals.ThisAddIn != null ? // This check is only necessary for the unit tests in which Globals.ThisAddIn is not defined
                    ReminderOutlookUtils.LoadChannels(new OutlookApplication(Globals.ThisAddIn.Application), /*restoreDefaultsIfNecessary*/true) :
                    new List<NotificationChannel>();
                
                _reminderItem = new ReminderItem(channels);
            }

            // Reset the flag that keeps track of changes to the enhanced reminder
            _reminderChanged = false;
        
            // Once we have loaded/created a ReminderItem, we attempt to extract conference information
            // from the calendar item
            ParseConferenceInformation();
        }

        /// <summary>
        /// Extracts conference information from the AppointmentItem interface
        /// </summary>
        private void ParseConferenceInformation()
        {
            Debug.Assert(_appointment != null, "_appointment != null");
            ConferenceInfo conferenceAppointment = new ConferenceInfo(_appointment.Body);

            // Populate data structure with necessary information. 
            Debug.Assert(conferenceAppointment != null, "conferenceAppointment != null");
            Debug.Assert(_reminderItem != null, "_reminderItem != null");

            _reminderItem.ConferenceAddress = conferenceAppointment.ConferenceAddress;
            _reminderItem.ConferenceType = conferenceAppointment.ConferenceType;

            // Assert that if ConferenceType == ConferenceType.None, then the ConferenceAddress must be null
            Debug.Assert(_reminderItem.ConferenceType == ConferenceType.None || _reminderItem.ConferenceAddress != null,
                "_reminderItem.ConferenceType == ConferenceType.None || _reminderItem.ConferenceAddress != null");
        }

        /// <summary>
        /// Execute the potentially time-consuming operation of deserializing ReminderItem from the attachment
        /// or serializing the changes to the attachment.
        /// We do this using the BackgroundWorker model to prevent it from freezing the main UI
        /// </summary>
        private void ExecuteBackgroundOperations(object sender, DoWorkEventArgs e)
        {
            Debug.Assert(sender != null, "sender != null");
            Debug.Assert(e != null, "e != null");

            if (Helpers.AssertCast<bool>(e.Argument))
            {
                SaveChanges();
            }
            else
            {
                // Start the time-consuming operation.
                LoadReminder();
            }
        }

        /// <summary>
        /// Sets the flag indicating that background worker has completed its
        /// operation and can be disposed
        /// </summary>
        private void BackgroundOperationsCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            Debug.Assert(sender != null, "sender != null");
            Debug.Assert(e != null, "e != null");

            Debug.Assert(_backgroundWorkerCompletedEvent != null, "_backgroundWorkerCompletedEvent != null");

            // Now we refresh the controls so that they're enabled as appropriate
            if (_ribbon != null && _reminderItem != null)
            {                
                _ribbon.RefreshControls();
            }

            // Sets the AutoResetEvent object which can be waited on by the
            // AppointmentWrite method or the OutlookWindow_Closed method
            _backgroundWorkerCompletedEvent.Set();
        }
        
        #endregion 

        #region IExchangeSession Members

        /// <summary>
        /// Returns Exchange connection mode
        /// </summary>
        public OlExchangeConnectionMode ConnectionMode
        {
            get
            {
                Debug.Assert(_window.Application != null, "_window.Application != null");
                Debug.Assert(_window.Application.Session != null, "_window.Application.Session != null");
                return _window.Application.Session.ExchangeConnectionMode;
            }
        }

        #endregion

        #region IDisposable Members

        /// <summary>
        /// Implement IDisposable because this class creates members of the IDisposable types
        /// </summary>
        public void Dispose()
        {
            if (_backgroundWorker != null)
            {
                _backgroundWorker.Dispose();
                _backgroundWorker = null;
            }

            if (_backgroundWorkerCompletedEvent != null)
            {
                _backgroundWorkerCompletedEvent.Close();
                _backgroundWorkerCompletedEvent = null;
            }
        }

        #endregion
    }
}
